#!/usr/bin/python

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

# Once again, let's code Handy
VERSION     =   "0.02"
LAST_UPDATE =   "16 July 2007"

# 0.01 -    Basic structure set up
# 0.02 -    Basic structure and ideas now seem to work

# Started this at the end of June 2008
# Initial interpreter code looks good
# Need to split so we get full interpretation followed by code run 
# *DONE*
# Add some command-line options
# *DONE*
# Add ~ and ~~ commands
# ~ is an actual command
# *DONE*
# ~~ is compiler sugar
# add =
# *DONE*
# make all the list commands work
# make the display function traverse lists
# and while
# Work out ! somewhere in there

import sys,re

#------------------------------------------------------------------------------
# code from here to next --- line could (should?) be written in C for speed

# we define the basics of Handy
# a var is special unit that holds either an element or a list

HElement    =   0
HList       =   1
HString     =   2

class HandyUnit:
    def __init__(self,initVar,unit,name=None):
        self.data=initVar
        self.HType=unit
        if(name==None):
            self.var=False
            self.name="_None_"
        else:
            # it really is a var
            self.var=True
            self.name=name
    
    # this could hold an element or list, or the same with a name
    # so there are operations that can be done here
    def push(self,data):
        # if we push to an element, it becomes a list:
        if(self.HType==HElement):
            self.HType=HList
            foo=HandyUnit(self.data,HElement)
            self.data=[foo,data]
        elif(self.HType==HList):
            # easy code
            self.data.append(data)
        return(self.data)
    
    def pop(self):
        # if we pop an element, it becomes E
        if(self.HType==HElement):
            foo=HandyUnit(self.data,HElement)
            self.HType=HList
            self.data=[]
            return(foo)
        # must be a list
        # return E if there is no data
        if(len(self.data)>0):
            return(self.data.pop())
        else:
            # return E
            return(HandyUnit([],HList))

    def append(self,data):
        """As push, but to the start of the list and not the end"""
        return(HandyUnit([],HList))

    def chop(self,data):
        """As pop, but to the start of the list and not the end"""
        return(HandyUnit([],HList))

    def display(self):
        """Output details. Used for debugging"""
        if(self.var==True):
            print "Variable:",self.name
        else:
            print "Not a variable"
        print "  HType=",
        if(self.HType==HElement):
            print "HElement"
        elif(self.HType==HList):
            print "HList"
        elif(self.HType==HString):
            print "HString"
        else:
            print "Unknown"
            # whoops.. better not print the data
            return(False)
        print "  Data:",self.data
        return(True)

# define the main Handy class

class Handy:
    def __init__(self):
        self.cycles=0
        self.var_list=[]
        # we always have 2 vars, the empty list
        self.SetVar(HandyUnit([],HList,"E"))
        self.e=self.var_list[0]
        # and a truth value (E also means false)
        # note we never return E or True themselves, otherwise code in the form of
        # (push (pop) 1) would set E to the value of 1! But you can do it yourself
        # with the code (= E 1).
        self.SetVar(HandyUnit(0,HList,"True"))
        self.true=self.var_list[1]

    def VarExists(self,name):
        """Return True if the var exists, False otherwise"""
        for i in self.var_list:
            if(i.name==name):
                return(True)
        # no such var
        return(False)
    
    def GetVar(self,name):
        """Return the given variable's data. If it doesn't exist, create it
           with a value of E and then return it"""
        for i in self.var_list:
            if(i.name==name):
                return(i)
        # no match, so create the new var with value E
        newvar=HandyUnit([],HList,name)
        self.var_list.append(newvar)
        # have to return this list, but it'll always be at the
        # end of the current variable list, thankfully
        return(self.var_list[-1])

    def SetVar(self,var):
        """Add a variable to the var list"""
        # if the var exists, just change the value
        if(self.VarExists(var.name)):
            # if it's set to [], you can delete that variable
            # TODO: really put here, or is that wrong?
            if(var.data==self.e):
                # delete from the list
                self.RemoveVar(var.name)
                return(True)
            # set the data
            self.GetVar(var.name).data=var.data
            # and the datatype
            self.GetVar(var.name).HType=var.HType
            return(True)
        # otherwise, add it as a new var
        self.var_list.append(var)
        return(True)

    def RemoveVar(self,name):
        """Remove the named var from the list"""
        i=0
        for i in self.var_list:
            # check each variable name
            if(i.name==name):
                # found it, remove this index and leave
                self.var_list.pop(i)
                return(True)
            # count for next index
            i+=1
        # couldn't find a match
        return(False)

    def ReduceToElement(self,data):
        """Given data, either a list or an HandyUnit Element, return
           it as an element
           If an element, then just return it
           If a list, pop first item and call again
           If the empty list, return an element of value 0
           Always returns an HandyUnit Element"""
        # is it easy?
        if(data.HType==HElement):
            return(data)
        elif(data.HType==HList):
            if(data.data==[]):
                # empty list
                return(HandyUnit(0,HElement))
            else:
                return(self.ReduceToElement(data.data[0]))
                
    def ConvertToList(self,data):
        """Convert elements to a list. You must call with a HandyUnit
           Always returns a list"""
        # an errors, return E
        if(data.HType!=HElement):
            return(HandyUnit([],HList))
        else:
            newList=HandyUnit([],HList)
            newList.data.append(HandyUnit(data.data,HElement))
            return(newList)

    # what follows are the in-built Handy commands

    # the most important function of all, really, the ~ instruction
    def buildlist(self,arguments):
        """Build a list out of the compenents
           So (~ 1 2 3) returns (1 2 3)"""
        # this should be a fairly easy routine
        nList=HandyUnit([],HList)
        # no args will produce an empty list, of course
        for i in arguments:
            nList.data.append(i)
        return(nList)

    # the = function is pretty important as well
    def equal(self,arguments):
        """equal sets variables to a given value. The last value
           is what they get set to, and any other arguments are set
           to this value if they are HVars, otherwise they are
           ignored
           Always returns the set value"""
        # enough arguments?
        if(len(arguments)<2):
            # return empty list
            return(HandyUnit([],HList))
        # otherwise, set all given vars to the value of the last one
        newValue=arguments.pop()
        for i in arguments:
            if(i.var==True):
                self.SetVar(HandyUnit(newValue.data,newValue.HType,i.name))
        return(newValue)

    # next come the numerous list functions
    def push(self,arguments):
        """Performs the function push, returns the new list        
           (push X Nx ... Ny) ->
           for all Nx from Nx -> Ny, in turn:
           if X and Nx are both elements, returns (X Nx)
           if X and Nx are both lists, returns (X (Nx))
           if X is a list and Nx an element, returns (X Nx)
           if X is an element and Nx a list, returns (X (Nx))"""
        # enough arguments?
        if(len(arguments)==0):
            # no args, an empty list
            return(HandyUnit([],HList))
        elif(len(arguments)==1):
            # 1 arg, return that (basically does nothing)
            return(arguments[0])
        # must be 2 args or more, lets loop for them all:
        # work out what the types are
        first=arguments.pop(0)
        # simply add the data to it
        while(len(arguments)>0):
            first.push(arguments.pop(0))
        return(first)       

    def pop(self,arguments):
        """Performs the function pop, returns the popped item
           (pop X ...) ->
           Ignores all arguments other than the first one
           If X is an element, return that element
           If X is a list, return the first item of that list
           If X is equal to E, return E
           If no argument, return E"""
        # any arguments?
        if(len(arguments)==0):
            return(HandyUnit([],HList))
        # should be ok, grab the first argument and work with that
        # just check it's not an empty list
        foo=arguments.pop(0)
        return(foo.pop())       
           
    # the standard math routines ignore lists in their arguments
    # so (+ 3 4 (1 2) E 6)
    # would produce (+ 3 4 6)

    def add(self,arguments):
        """Adds elements, returns the result
           no args - answer=0, 1 arg, answer=arg
           Always returns an element"""
        # source should be a series of elements. A real list is 0
        result=0
        for i in arguments:
            # if it's a real element, add it's value, else do nothing
            if(i.HType==HElement):
                result+=i.data              
        # return the result
        return(HandyUnit(result,HElement))

    def sub(self,arguments):
        """Subtract source from destination
           No args - answer=0, 1 arg - answer=-arg
           Always returns an element"""
        # abnormal results on only 0 or 1 arguments
        if(len(arguments)==0):
            return(HandyUnit(0,HElement))
        elif(len(arguments)==1):
            return(HandyUnit(self.ReduceToElement(arguments.pop())*-1).HElement)
        # grab first one:
        foo=arguments.pop(0)
        if(foo.HType==HElement):
            result=foo.data
        else:
            result=0
        for i in arguments:
            if(i.HType==HElement):
                result-=i.data
        # finally, return the result
        return(HandyUnit(result,HElement))

    def mul(self,arguments):
        """Multiply together arguments
           No args - answer=0, 1 arg - answer=arg
           Always returns an element"""
        # test for <2 args
        if(len(arguments)==0):
            return(HandyUnit(0,HElement))
        elif(len(arguments)==1):
            return(HandyUnit(self.ReduceToElement(arguments.pop()).HElement))
        foo=arguments.pop(0)
        if(foo.HType==HElement):
            result=foo.data
        else:
            # set to 1 else result will always equal 0
            result=1
        for i in arguments:
            if(i.HType==HElement):
                result=result*i.data
        # return the result
        return(HandyUnit(result,HElement))

    def div(self,arguments):
        """Divide arguments, i.e. (/ a b c) => a / b / c
           No args - answer= 0, 1 arg - answer=arg
           divide by zero = E
           Always returns an element"""
        # test for <2 args
        if(len(arguments)==0):
            return(HandyUnit(0,HElement))
        elif(len(arguments)==1):
            return(HandyUnit(self.ReduceToElement(arguments.pop(),HElement)))
        foo=arguments.pop(0)
        if(foo.HType==HElement):
            result=foo.data
        else:
            # set to 1 to avoid div by zero
            result=1
        for i in arguments:
            if(i.HType==HElement):
                if(i.data!=0):
                    # note that we only deal with ints so far
                    result=int(result/i.data)
                else:
                    # div by zero
                    return(HandyUnit([],HList))
        # return the result
        return(HandyUnit(result,HElement))

    def display(self,arguments):
        """Print out a litteral format of what we have"""
        for foo in arguments:
            # for each bit of data:
            if(foo.HType==HElement):
                print "E:",foo.data
            elif(foo.HType==HString):
                # we should print out the value of the var
                print "Var:",foo.data
            else:
                print "L: (",
                for i in foo.data:               
                    if(i.HType==HElement):
                        print i.data,
                    elif(i.HType==HString):
                        print i.data,
                    else:
                        print "L",
                print ")"
        # return something, at least
        # we use var 1 which is at least True
        return(self.true)

    def idisp(self,arguments):
        """Display the internal makeup of the first argument
           Debug function, always returns E"""
        arguments[0].display()
        return(HandyUnit([],HList))

    # next routine acts as the 'sorter' routine
    def DoFunction(self,data):
        """Work out what function is being called and do it
           Two important points in this function - we have to return
           the value passed (which will be an HObject of some kind)
           And also we have to convert var names to real vars, and
           pass the var rather than the text"""

        # important code!
        # we should get here a HandyElement. It has to be:
        # a built-in function (name in a HString - test last)
        # a var (name in a HString - test first)
        # a HElement (test after vars)
        
        # first point of data IS the function
        function=data.pop(0)
        
        # we now have to convert any variables given to actual lists
        # at the moment, they are simple HStrings. We will convert these strings
        # so that the data now points at the variable
        # this means that any calls from this point on will *not* see a HString

        cdata=[]
        for i in data:
            if(i.HType==HString):
                # do the conversion
                cdata.append(self.GetVar(i.data))
            else:
                # just add the data
                cdata.append(i)
        data=cdata
        
        # function is normally a HString
        if(function.HType==HString):
            # test for vars first
            # then test for built-in functions
            # the all-important ~
            if(function.data=="~"):
                return(self.buildlist(data))
            # and of course =
            elif(function.data=="="):
                return(self.equal(data))

            # then the list functions
            elif(function.data=="push"):
                return(self.push(data))
            elif(function.data=="pop"):
                return(self.pop(data))

            # then the basic math stuff
            elif(function.data=="+"):
                return(self.add(data))
            elif(function.data=="-"):
                return(self.sub(data))
            elif(function.data=="*"):
                return(self.mul(data))
            elif(function.data=="/"):
                return(self.div(data))
            
            # some helper routines
            elif(function.data=="display"):        
                return(self.display(data))
            elif(function.data=="idisp"):
                return(self.idisp(data))
        
            else:
                print "[Error]: Unknown function '"+function.data+"' called"
                # bad error, exit
                print "         Exiting Handy"
                sys.exit(False)
              
        # I suppose it could be a list, so cover that
        elif(function.HType==HList):
            return(self.ReduceToElement(function))
        # function actually an element? then return it
        elif(function.HType==HElement):
            return(function)
        else:
            print "[Error]: Unknown data called, got:",function
            # just return E
            return(HandyUnit([],HList))

    def ExecuteList(self,code):
        """Call with a HandyUnit list. Executes the code"""
        # start by trying to build up a list with no other lists:
        nList=[]
        for i in code.data:
            # either it's a list, in which case we just call
            # this code again, or we add it to our list
            if(i.HType==HList):
                nList.append(self.ExecuteList(i))
            # otherwise add the element or HString
            else:
                nList.append(i)
        return(self.DoFunction(nList))


# end of Handy engine code
#------------------------------------------------------------------------------

# set a general routine to parse a file and build a list from it

class FileParser:
    def __init__(self):
        self.linenumber=-1
        self.end_file=False
        self.line_tokens=[]
        self.exit=True
        self.engine=Handy()

    def GetNextLine(self):
        """Grab next line and turn into tokens"""
        self.line_tokens=[]
        # loop until we have at least 1 token
        while(len(self.line_tokens)==0):
            current_line=self.txtfile.readline()
            self.linenumber+=1
            if(current_line==""):
                # we've hit the end of the file
                self.end_file=True
                if(self.exit==False):
                    # this is an abnormal exit
                    print "[Error]: Unexpected end of file at line",self.linenumber
                    sys.exit(False)
                return(False)
         
            # routine splits along ( and ), but these need to be seperate
            # i.e. if we have a token with "))", it will need to be ") )"
            # so we add the spaces here
            while(current_line.count("))")>0):
                i=current_line.find("))")
                current_line=current_line[:i+1]+" "+current_line[i+1:]
            # repeat for ((
            while(current_line.count("((")>0):
                i=current_line.find("((")
                current_line=current_line[:i+1]+" "+current_line[i+1:]
            
            # found a line, so split the tokens
            pass_one=current_line.split()           
            # for each token, continue to split along ( and )
            pass_two=[]
            for foo in pass_one:
                line=foo.split('(')
                # after a split, a double space means only a single
                # space, really (it's the way split works!)
                if(line==['','']):
                    line=line[1:]  
                for i in line:
                    if(i==""):
                        pass_two.append("(")
                    else:
                        pass_two.append(i)
            for bar in pass_two:
                line=bar.split(')')
                # after a split, a double space means only a single
                # space, really (it's the way split works!)
                if(line==['','']):
                    line=line[1:]              
                for i in line:
                    if(i==""):
                        self.line_tokens.append(")")
                    else:
                        self.line_tokens.append(i)
	        # just a comment?
            if((len(self.line_tokens)>0)and(re.match("^#.*$",self.line_tokens[0]))): 
	            # then ignore this line
	            self.line_tokens=[]
	    return(True)
	
    def ReadToken(self):
        """Reads in next token from either the file or the string"""
        while(len(self.line_tokens)==0):
            # no tokens, so get the next line first
            if(self.GetNextLine()==False):
                # must be end of file                
                return(False)
        # now get the last token
        self.token=self.line_tokens.pop(0)
        return(True)            

    def ParseString(self):
        """Starting with a (, continue till the end and
           then construct the string"""
        # loop through and find the list, then reconstruct it
        new_list=HandyUnit([],HList)
        exit=False
        while(exit==False):
            self.ReadToken()
            if(self.token=="("):
                new_list.data.append(self.ParseString())
            elif(self.token==")"):
                # we're at the end of a list, exit this loop
                exit=True
            else:
                # not a bracket, so add it
                # find out what it is at this point.
                # is it a plain number?
                if(self.token.isdigit()==True):
                    new_list.data.append(HandyUnit(int(self.token),HElement))
                else:
                    # must be a function or a var, add the plain string
                    new_list.data.append(HandyUnit(self.token,HString))
        # now we know the result, return it
        return(new_list)

    def LoadCode(self,filename):
        """Loads given code and converts to a list"""
        # check for bad filename first
        try:
            self.txtfile=open(filename)
        except IOError:
            print "[Error]: No such file '",filename,"'"
            return(False)
        self.end_file=False
        while((self.ReadToken()==True)):
            # it *has* to be a (
            if(self.token!="("):
                print "[Error]: Expected list, got ",self.token," on line ",self.linenumber
            # we can only exit on this loop
            self.exit=False
            # go and compute it
            result=self.ParseString()
            # now perform the code
            self.engine.ExecuteList(result)
            self.exit=True
        self.txtfile.close()
        return(True)

# start by handling command line options
def main():
    # get command line options
    options=sys.argv
	# get the arguments
    if(len(options)==1):
        # display standard message
        print "Handy Language v"+VERSION
        print "  Code & Design Chris Smith 2007"
        print "  Usage: handy -filename.handy"
        return(False)
    if(len(options)!=2):
        print "[Error]: Too many arguments passed!"
        return(False)
	# remove the first one
    filename=options.pop()
    HandyParser=FileParser()
    HandyParser.LoadCode(filename)
    return(True)

# start of code
if __name__=="__main__":
    main()

